package com.adamfass.tractorbeam;

import java.util.ArrayList;
import java.util.List;

import com.jme.input.KeyBindingManager;
import com.jme.input.KeyInput;
import com.jme.math.Vector2f;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jme.scene.shape.Sphere;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.MaterialState;
import com.jme.scene.state.ZBufferState;
import com.jme.scene.state.MaterialState.MaterialFace;
import com.jme.system.DisplaySystem;
import com.jmex.font2d.Font2D;
import com.jmex.font2d.Text2D;

final class MenuUtil {
  static final float DISPLAY_WIDTH = 400;
  static final float DISPLAY_HEIGHT = 300;
  
  static final String COMMAND_NEXT_ITEM = "COMMAND_NEXT_ITEM"; 
  static final String COMMAND_PREVIOUS_ITEM = "COMMAND_PREVIOUS_ITEM";
  static final String COMMAND_SELECT_ITEM = "COMMAND_SELECT_ITEM";
  
  private MenuUtil() {}
  
  static Vector3f fractionalToScreen3D(float x, float y) {
    final DisplaySystem displaySystem = DisplaySystem.getDisplaySystem();
    return new Vector3f(
        (displaySystem.getWidth() / 2) + (x * displaySystem.getWidth()),
        (displaySystem.getHeight() / 2) + (y * displaySystem.getHeight()),
        0);
  }
  
  static Vector2f fractionalToScreen(float x, float y) {
    final DisplaySystem displaySystem = DisplaySystem.getDisplaySystem();
    return new Vector2f(
        (displaySystem.getWidth() / 2) + (x * displaySystem.getWidth()),
        (displaySystem.getHeight() / 2) + (y * displaySystem.getHeight()));
  }
  
  static void setLocalTranslationFromFractionalCoordinates(
      Text2D text, float x, float y, float scale) {
    final DisplaySystem displaySystem = DisplaySystem.getDisplaySystem();
    text.setLocalTranslation(new Vector3f(
        (displaySystem.getWidth() / 2) + (x * displaySystem.getWidth())
        - ((text.getWidth() / 2) * scale), 
        (displaySystem.getHeight() / 2) + (y * displaySystem.getHeight())
        - ((text.getHeight() / 2) * scale),
        0));
  }
  
  static Text2D add2DText(
      Node rootNode, Font2D font, String text) {
    final ZBufferState zBufferState = 
      DisplaySystem.getDisplaySystem().getRenderer().createZBufferState();
    zBufferState.setFunction(ZBufferState.TestFunction.Always);
    final Text2D text2D = font.createText(text, 1, 0);
    text2D.setRenderQueueMode(Renderer.QUEUE_ORTHO);
    text2D.setRenderState(zBufferState);
    rootNode.attachChild(text2D);
    return text2D;
  }
  
  static Vector3f screenToWorld(Vector2f screen) {
    final float width = DisplaySystem.getDisplaySystem().getWidth();
    final float height = DisplaySystem.getDisplaySystem().getHeight();
    return new Vector3f(
        ((screen.x - (width / 2)) / width) * DISPLAY_WIDTH,
        ((screen.y - (height / 2)) / height) * DISPLAY_HEIGHT,
        0);
  }
}


class Menu {
  private static final class InternalMenuItem {
    private final Runnable action;
    private final Vector3f cursorTranslation;
    
    public InternalMenuItem(
        Text2D text, Runnable action, Vector3f cursorTranslation) {
      this.action = action;
      this.cursorTranslation = cursorTranslation;
    }
    
    public void runAction() {
      action.run();
    }
  }

  private final List<InternalMenuItem> internalMenuItems;
  private int selectedMenuItem;
  final Spatial cursor;
  final float fractionalX;
  final float fractionalY;
  
  public Menu(
      Node rootNode, Font2D font, Spatial cursor, float x, float y, float scale, 
      MenuItem... menuItems) {
    assert menuItems.length > 0;
    
    //  A scale of 2 results in the font showing at it's natural resolution.
    scale *= 2;
    
    fractionalX = x;
    fractionalY = y;
    
    this.internalMenuItems = new ArrayList<InternalMenuItem>();
    float maxItemWidth = 0;
    float maxItemHeight = 0;
    final Text2D[] text = new Text2D[menuItems.length];
    for (int i = 0; i < menuItems.length; i++) {
      MenuItem item = menuItems[i];
      text[i] = MenuUtil.add2DText(rootNode, font, item.text);
      maxItemWidth = Math.max(maxItemWidth, text[i].getWidth() * scale);
      maxItemHeight = Math.max(maxItemHeight, text[i].getHeight() * scale);
    }
    
    maxItemHeight += 10;  // Padding between menu items
    final float cursorWidth = maxItemHeight;
    final float totalHeight = maxItemHeight * menuItems.length;
    
    for (int i = 0; i < menuItems.length; i++) {
      text[i].setLocalTranslation(
          MenuUtil.fractionalToScreen3D(x, y)
          .subtract(maxItemWidth / 2, 0, 0)
          .add(0, (totalHeight / 2) - ((i + 1) * maxItemHeight), 0));
      text[i].setLocalScale(scale);
      
      final Vector2f cursorTranslationScreen = 
          MenuUtil.fractionalToScreen(x, y)
          .subtract(new Vector2f(((maxItemWidth + cursorWidth) / 2), 0f))
          .add(new Vector2f(0, (totalHeight / 2) - ((i + 0.5f) * maxItemHeight)));
      
      // This should work, but it makes the VM crash
      // final Vector3f cursorTranslationWorld = DisplaySystem.getDisplaySystem()
      //   .getWorldCoordinates(cursorTranslationScreen, 1);
      
      final Vector3f cursorTranslationWorld = 
        MenuUtil.screenToWorld(cursorTranslationScreen);
         
      final InternalMenuItem internalItem = 
        new InternalMenuItem(text[i], menuItems[i].action, cursorTranslationWorld);
      internalMenuItems.add(internalItem);
    }
    
    selectedMenuItem = 0;
    this.cursor = cursor;
    
    cursor.setLocalTranslation(internalMenuItems.get(0).cursorTranslation);
    rootNode.attachChild(cursor);
    
    KeyBindingManager.getKeyBindingManager().add(MenuUtil.COMMAND_NEXT_ITEM, 
        KeyInput.KEY_DOWN);
    KeyBindingManager.getKeyBindingManager().add(MenuUtil.COMMAND_NEXT_ITEM, 
        KeyInput.KEY_RIGHT);
    
    KeyBindingManager.getKeyBindingManager().add(MenuUtil.COMMAND_PREVIOUS_ITEM, 
        KeyInput.KEY_UP);
    KeyBindingManager.getKeyBindingManager().add(MenuUtil.COMMAND_PREVIOUS_ITEM, 
        KeyInput.KEY_LEFT);
    
    KeyBindingManager.getKeyBindingManager().add(MenuUtil.COMMAND_SELECT_ITEM, 
        KeyInput.KEY_RETURN);
    KeyBindingManager.getKeyBindingManager().add(MenuUtil.COMMAND_SELECT_ITEM, 
        KeyInput.KEY_SPACE);    
  }
  
  public void update(float timePerFrame) {
    if (KeyBindingManager.getKeyBindingManager().isValidCommand(
        MenuUtil.COMMAND_NEXT_ITEM, false)) {
      nextItem();
    } else if (KeyBindingManager.getKeyBindingManager().isValidCommand(
        MenuUtil.COMMAND_PREVIOUS_ITEM, false)) {
      previousItem();
    } else if (KeyBindingManager.getKeyBindingManager().isValidCommand(
        MenuUtil.COMMAND_SELECT_ITEM, false)) {
      selectCurrentItem();
    }
  }
  
  private void nextItem() {
    selectedMenuItem++;
    if (selectedMenuItem == internalMenuItems.size()) {
      selectedMenuItem = 0;
    }
    cursor.setLocalTranslation(internalMenuItems.get(selectedMenuItem)
        .cursorTranslation);
  }
  
  private void previousItem() {
    selectedMenuItem--;
    if (selectedMenuItem == -1) {
      selectedMenuItem = internalMenuItems.size() - 1;
    }
    cursor.setLocalTranslation(internalMenuItems.get(selectedMenuItem)
        .cursorTranslation);
  }
  
  private void selectCurrentItem() {
    internalMenuItems.get(selectedMenuItem).runAction();
  }
}

class MenuItem {
  public final String text;
  public final Runnable action;
  
  public MenuItem(String text, Runnable action) {
    this.text = text;
    this.action = action;
  }
}

abstract class MenuScreen {
  final Font2D font;
  final ZBufferState zBufferState;
  Menu menu;
  
  protected MenuScreen() {
    font = new Font2D();
    
    zBufferState = 
      DisplaySystem.getDisplaySystem().getRenderer().createZBufferState();
    zBufferState.setFunction(ZBufferState.TestFunction.Always);
  }
  
  public abstract void setUp(Node rootNode);
  
  public void update(float timePerFrame) {
    if (menu != null) {
      menu.update(timePerFrame);
    }
  }
  
  public void addLabel(Node rootNode, String text, float x, float y, float scale) {
    // A scale of 2 results in the font showing at it's natural resolution.
    scale *= 2;
    
    final Text2D text2D = font.createText(text, 1, 0);
    text2D.setRenderQueueMode(Renderer.QUEUE_ORTHO);
    text2D.setRenderState(zBufferState);
    MenuUtil.setLocalTranslationFromFractionalCoordinates(
        text2D, x, y, scale);
    text2D.setLocalScale(scale);
    rootNode.attachChild(text2D);    
  }

  public void addMenu(Node rootNode, 
                      Spatial cursor, 
                      float x, 
                      float y,
                      float scale,
                      MenuItem... menuItems) {
    menu = new Menu(rootNode, font, cursor, x, y, scale, menuItems);
  }
}

class TitleScreen extends MenuScreen {
  private final MenuTree menuTree;
  
  public TitleScreen(MenuTree menuTree) {
    this.menuTree = menuTree;
  }
  
  public void setUp(Node rootNode) {    
    addLabel(rootNode, "Tractor Beam Ball", 0, 0.25f, 1);
    addLabel(rootNode, "By Adam M. Fass", 0, 0.15f, 0.5f);
    
    final Node cursorNode = new Node("cursor");
    cursorNode.setLocalScale(6);
    
    Sphere disc = new Sphere("saucer disc", 10, 20, 1);
    disc.setLocalScale(new Vector3f(1, 0.25f, 1));
    disc.setLocalTranslation(0, -0.25f, 0);
    final MaterialState discMaterialState = 
      DisplaySystem.getDisplaySystem().getRenderer().createMaterialState();
    discMaterialState.setEnabled(true);
    discMaterialState.setDiffuse(ColorRGBA.green);
    discMaterialState.setAmbient(ColorRGBA.green);
    disc.setRenderState(discMaterialState);
    cursorNode.attachChild(disc);
    
    Sphere bubble = new Sphere("saucer bubble", 10, 10, 0.4f);
    bubble.setLocalTranslation(0, 0, 0);
    bubble.setRenderQueueMode(Renderer.QUEUE_TRANSPARENT);
    final MaterialState bubbleMaterialState = 
      DisplaySystem.getDisplaySystem().getRenderer().createMaterialState();
    bubbleMaterialState.setEnabled(true);
    bubbleMaterialState.setAmbient(new ColorRGBA(0.5f, 0.5f, 1, 0.25f));
    bubbleMaterialState.setDiffuse(new ColorRGBA(0.5f, 0.5f, 1, 0.25f));
    bubbleMaterialState.setSpecular(new ColorRGBA(1, 1, 1, 0.25f));
    bubbleMaterialState.setShininess(10);
    bubbleMaterialState.setMaterialFace(MaterialFace.FrontAndBack);
    bubble.setRenderState(bubbleMaterialState);
    cursorNode.attachChild(bubble);
    
    final BlendState bubbleBlendState = 
      DisplaySystem.getDisplaySystem().getRenderer().createBlendState();
    bubbleBlendState.setBlendEnabled(true);
    bubbleBlendState.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
    bubbleBlendState.setDestinationFunction(BlendState.DestinationFunction.One);
    bubbleBlendState.setTestEnabled(true);
    bubbleBlendState.setTestFunction(BlendState.TestFunction.GreaterThan);
    bubbleBlendState.setEnabled(true);
    bubble.setRenderState(bubbleBlendState);
    
    addMenu(
        rootNode,
        cursorNode,
        0f, 0f,
        0.75f,
        new MenuItem("1 Player Vs. Computer", new Runnable() {
          public void run() {
            menuTree.beginGame(new ClassicGameFlow(
                ClassicGameFlow.GameType.ONE_PLAYER_VS_COMPUTER));
          }
        }),
        new MenuItem("Multiplayer", new Runnable() {
          public void run() {
            menuTree.beginGame(new ClassicGameFlow(
                ClassicGameFlow.GameType.MULTIPLAYER));
          }
        }),
        new MenuItem("Experimental", new Runnable() {
          public void run() {
            menuTree.beginGame(new GoalGameFlow(
                GoalGameFlow.GameType.ONE_PLAYER_VS_COMPUTER));
          }
        }));
  }
}

class MenuTree {
  public interface MenuTreeListener {
    public void handleBeginGame(GameFlow gameFlow);
  }
  
  private final MenuTreeListener listener;
  private MenuScreen currentScreen;
  private final TitleScreen titleScreen;
  
  public MenuTree(MenuTreeListener listener, Node rootNode) {
    this.listener = listener;
    
    titleScreen = new TitleScreen(this);
    titleScreen.setUp(rootNode);
    
    currentScreen = titleScreen;
  }
  
  public void udpate(float timePerFrame) {
    final Camera camera = 
      DisplaySystem.getDisplaySystem().getRenderer().getCamera();
    
    // http://www.jmonkeyengine.com/wiki/doku.php?id=parallel_versus_perspective_projection
    // http://www.jmonkeyengine.com/jmeforum/index.php?topic=5104.msg41269#msg41269
    camera.setParallelProjection(true);
    camera.setLocation(new Vector3f(0, 0, 1000));
    camera.setDirection(new Vector3f(0, 0, -1));
    camera.setFrustum(
        -100, 2000, 
        -(MenuUtil.DISPLAY_WIDTH / 2), (MenuUtil.DISPLAY_WIDTH / 2), 
        -(MenuUtil.DISPLAY_HEIGHT / 2), (MenuUtil.DISPLAY_HEIGHT / 2));
    
    currentScreen.update(timePerFrame);   
  }
  
  public void gotoTitleScreen() {
    currentScreen = titleScreen;
  } 
  
  public void beginGame(GameFlow gameFlow) {
    listener.handleBeginGame(gameFlow);
  }
}